#region File Header
//-----------------------------------------------------------------------------
// Bricks' Bane
//
// Copyright (C) 2010 Julien Villers
// This program is distributed under the terms of the 
// GNU Lesser General Public License (LGPL).
// See Docs/lgpl.txt and Docs/gpl.txt
//-----------------------------------------------------------------------------
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using BricksBane.Entities;
using BricksBane.Screens;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using SibLib;
using SibLib.Containers;
using SibLib.GameState;
using SibLib.Render;
using SibLib.Sound;
using SibLib.Text;

namespace BricksBane
{
    #region Types
    /// <summary>
    /// List possible game modes.
    /// </summary>
    enum E_GameModes
    {
        Campaign = 0,
        ScoreAttack,
        TimeAttack,
        Marathon,

        Count_
    };

    /// <summary>
    /// List possible game difficulty levels.
    /// </summary>
    enum E_GameDifficulties
    {
        Easy = 0,
        Medium,
        Hard,

        Count_
    };

    /// <summary>
    /// Store the state of a play session.
    /// This is meant to be serialized when saving the game state.
    /// </summary>
    struct PlaySession
    {
        /// <summary>
        /// Number of lives at beginning of campaign, indexed by difficulty.
        /// </summary>
        private static readonly int[] LIVES_CAMPAIGN = { 6, 4, 3 };
        /// <summary>
        /// Number of lives at beginning of time attack, indexed by difficulty.
        /// </summary>
        private static readonly float[] TIMES = { 300.0f, 240.0f, 180.0f };

        private const float SPEED_MIN = 200.0f;
        private static readonly float[] SPEED_MAXES = {800.0f, 1000.0f, 1200.0f};

        #region Public Methods
        public PlaySession(E_GameModes mode, E_GameDifficulties difficulty)
        {
            m_Difficulty = difficulty;
            m_Mode = mode;
            m_LivesRemaining = LIVES_CAMPAIGN[(int)difficulty];     // TODO: other modes
            if (mode == E_GameModes.TimeAttack)
                m_SecondsRemaining = TIMES[(int)difficulty];
            else
                m_SecondsRemaining = 0.0f;
            m_LevelCurrentIndex = 0;
            m_Score = 0;
        }

        public float GetSpeedMin()
        {
            return SPEED_MIN;
        }

        public float GetSpeedMax()
        {
            return SPEED_MAXES[(int)m_Difficulty];
        }
        #endregion

        public int m_LivesRemaining;
        public float m_SecondsRemaining;
        public int m_Score;
        public E_GameDifficulties m_Difficulty;
        public E_GameModes m_Mode;
        public int m_LevelCurrentIndex;
    };

    /// <summary>
    /// Container for messages shown when collecting a bonus (aka Drop).
    /// </summary>
    class BonusMessage
    {
        public string m_Message;
        public float m_Timer;
        public Color m_Color;

        public const float MESSAGE_DURATION = 0.8f;
    }
    #endregion

    /// <summary>
    /// This screen implements the actual game logic.
    /// </summary>
    sealed class GameplayScreen : GameScreen
    {
        #region Members
        // Data
        private ContentManager m_Content;
        private SpriteFont m_GameFont;
        private SpriteFont m_GameItemsFont;

        // Render
        private SceneInfo m_SceneInfo;
        private SpriteBatch m_SpriteBatch;
        private PrimitiveBatch m_PrimitiveBatch;
        private Starfield m_Starfield;
        private Vector2 m_StarfieldPosition;
        private StringBuilder m_ScoreString;
        private StringBuilder m_TimeString;

        // Sound
        private ISoundManager m_SoundManager;

        // Gameplay
        private Wall m_Wall;
        private Paddle m_Paddle;
        private AutoPool<Ball> m_Balls;

        // Flow
        private List<string> m_LevelList;
        private PlaySession m_PlaySession;
        private string m_LevelName;

        // State
        private bool m_IsWaitingForLaunch;
        private AutoPool<BonusMessage> m_Messages;
        private bool m_IsVibrationRequired;
        #endregion

        #region Singleton
        public static GameplayScreen Instance
        {
            get;
            private set;
        }
        #endregion

        #region Properties
        public ContentManager Content
        {
            get { return m_Content; }
        }
        public SceneInfo SceneInfo
        {
            get { return m_SceneInfo; }
        }
        public GraphicsDevice GraphicsDevice
        {
            get { return ScreenManager.GraphicsDevice; }
        }
        public SpriteBatch SpriteBatch
        {
            get { return m_SpriteBatch; }
        }
        public SpriteFont SpriteFontGame
        {
            get { return m_GameFont; }
        }
        public SpriteFont SpriteFontItems
        {
            get { return m_GameItemsFont; }
        }
        public ISoundManager SoundManager
        {
            get { return m_SoundManager; }
        }

        public AutoPool<Drop> DropPool
        {
            get;
            private set;
        }
        public AutoPool<Ball> Balls
        {
            get { return m_Balls; }
        }

        public float BallSpeedMinimum
        {
            get { return m_PlaySession.GetSpeedMin(); }
        }
        public float BallSpeedMaximum
        {
            get { return m_PlaySession.GetSpeedMax(); }
        }
        #endregion

        #region Initialization

        /// <summary>
        /// Constructor.
        /// </summary>
        public GameplayScreen(E_GameModes mode, E_GameDifficulties difficulty)
        {
            if (Instance != null)
                throw new InvalidOperationException("Trying to create more than one instance of a singleton");
            Instance = this;

            TransitionOnTime = TimeSpan.FromSeconds(1.5);
            TransitionOffTime = TimeSpan.FromSeconds(1.0);

            m_PlaySession = new PlaySession(mode, difficulty);

            m_Paddle = new Paddle();
            m_Balls = new AutoPool<Ball>(12);       // TODO: tuning
            
            m_SceneInfo = new SceneInfo();

            DropPool = new AutoPool<Drop>(128);

            m_Messages = new AutoPool<BonusMessage>(32);

            m_ScoreString = new StringBuilder(16);
            m_TimeString = new StringBuilder(8);
        }

        /// <summary>
        /// Load graphics content for the game.
        /// </summary>
        public override void LoadContent()
        {
            if (m_Content == null)
                m_Content = new ContentManager(ScreenManager.Game.Services, "Content");

            m_GameFont = m_Content.Load<SpriteFont>("SpriteFontGame");
            m_GameItemsFont = m_Content.Load<SpriteFont>("SpriteFontGameItems");
            m_SpriteBatch = new SpriteBatch(GraphicsDevice);
            m_PrimitiveBatch = new PrimitiveBatch(GraphicsDevice);

            m_Starfield = new Starfield(Vector2.Zero, GraphicsDevice, m_Content);
            m_Starfield.LoadContent();

            m_SceneInfo.CreateOrthoMatrices(ScreenManager.Game);

            m_SoundManager = (ISoundManager)ScreenManager.Game.Services.GetService(typeof(ISoundManager));
            m_SoundManager.StartPlayList(new string[] { "SILENCE0", "GameMusic", "SILENCE5" });

            LoadLevelList();

            LoadLevelCurrent();

            InitializeGameplay();
            

            // once the load has finished, we use ResetElapsedTime to tell the game's
            // timing mechanism that we have just finished a very long frame, and that
            // it should not try to catch up.
            ScreenManager.Game.ResetElapsedTime();
        }

        /// <summary>
        /// Unload graphics content used by the game.
        /// </summary>
        public override void UnloadContent()
        {
            m_Starfield.UnloadContent();

            m_Content.Unload();

            Instance = null;
        }

        private void LoadLevelList()
        {
            string listName;
            m_LevelList = new List<string>(32);
            switch (m_PlaySession.m_Mode)
            {
                case E_GameModes.Campaign:
                    listName = m_PlaySession.m_Difficulty.ToString();
                    break;
                case E_GameModes.ScoreAttack:
                case E_GameModes.TimeAttack:
                    m_LevelList.Add("level01");         // TODO: support level selection
                    return;
                case E_GameModes.Marathon:
                    listName = "Marathon";
                    break;

                default:
                    return;
            }

            using (StreamReader reader = new StreamReader(m_Content.RootDirectory + "/LevelLists/" + listName + ".lpl"))
            {
                while (!reader.EndOfStream)
                    m_LevelList.Add(reader.ReadLine());
            }
        }

        private void LoadLevelCurrent()
        {
            Building.LevelBuilder builder = new Building.LevelBuilder();
            builder.Load(m_Content.RootDirectory + "/Levels/" + m_LevelList[m_PlaySession.m_LevelCurrentIndex] + ".lvl");
            
            m_Wall = builder.Wall;
            m_Wall.Initialize();

            m_LevelName = builder.LevelName;

            m_StarfieldPosition = RandomMath.RandomVector2(-1024.0f, 1024.0f);
            m_Starfield.ColorRatios = Color.Lerp(m_Wall.Color, Color.White, 0.5f).ToVector3();
        }

        private void InitializeGameplay()
        {
            ResetPaddle();
            DropPool.Clear();
            DropPool.ProcessRemoves();
            m_Messages.Clear();

            m_Balls.Clear();
            m_Balls.ProcessRemoves();
            Ball ball = m_Balls.AddNewItem();
            SetupBall(ball);
            ball.Spawn();

            m_ScoreString.Remove(0, m_ScoreString.Length);
            StringTools.AppendIntToStringBuilder(m_ScoreString, m_PlaySession.m_Score);

            UpdateTimeString();

            m_IsWaitingForLaunch = true;
        }

        #endregion

        #region Update

        /// <summary>
        /// Updates the state of the game. This method checks the GameScreen.IsActive
        /// property, so the game will stop updating when the pause menu is active,
        /// or if you tab away to a different application.
        /// </summary>
        public override void Update(GameTime gameTime, bool otherScreenHasFocus,
                                                       bool coveredByOtherScreen)
        {
            base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen);

            if (!IsActive)
                return;

            float timeFactor = gameTime.ElapsedGameTime.Milliseconds / 1000.0f;

            if (m_IsWaitingForLaunch)
            {
                // TODO: animation update.
            }
            else
            {
                m_Paddle.Update(timeFactor);

                m_Balls.ProcessAdds();
                foreach (Ball ball in m_Balls)
                {
                    ball.Update(timeFactor);
                    if (!ball.IsActive)
                        m_Balls.Remove(ball);
                }
                m_Balls.ProcessRemoves();
                if (m_Balls.Count == 0)
                {
                    OnLastBallLost();
                }

                m_Wall.Update(timeFactor);

                DropPool.ProcessAdds();
                foreach (Drop drop in DropPool)
                {
                    drop.Update(timeFactor);
                    if (!drop.IsActive)
                        DropPool.Remove(drop);
                }
                DropPool.ProcessRemoves();

                m_Messages.ProcessAdds();
                if (m_Messages.Count > 0)
                {
                    BonusMessage message = m_Messages[0];
                    message.m_Timer -= timeFactor;
                    if (message.m_Timer < 0)
                        m_Messages.RemoveAt(0);
                }
                m_Messages.ProcessRemoves();

                if (m_PlaySession.m_Mode == E_GameModes.TimeAttack)
                {
                    m_PlaySession.m_SecondsRemaining -= timeFactor;
                    if (m_PlaySession.m_SecondsRemaining <= 0.0f)
                    {
                        LoadingScreen.Load(ScreenManager, false, ControllingPlayer, new ResultScreen(m_PlaySession));
                        return;
                    }

                    UpdateTimeString();
                }
            }
        }
        #endregion

        #region Inputs

        /// <summary>
        /// Lets the game respond to player input. Unlike the Update method,
        /// this will only be called when the gameplay screen is active.
        /// </summary>
        public override void HandleInput(GameTime gameTime, InputState input)
        {
            if (input == null)
                throw new ArgumentNullException("input");

            // Look up inputs for the active player profile.
            int playerIndex = (int)ControllingPlayer.Value;

            KeyboardState keyboardState = input.CurrentKeyboardStates[playerIndex];
            GamePadState gamePadState = input.CurrentGamePadStates[playerIndex];

            // Check for pausing (pad disconnect, or button press).
            bool gamePadDisconnected = !gamePadState.IsConnected &&
                                       input.GamePadWasConnected[playerIndex];

            if (input.IsPauseGame(ControllingPlayer) || gamePadDisconnected
                || input.IsNewButtonPress(Buttons.Back, ControllingPlayer.Value)
                || input.IsNewButtonPress(Buttons.Start, ControllingPlayer.Value))
            {
                ScreenManager.AddScreen(new PauseMenuScreen(), ControllingPlayer);
                /*#warning Pause disabled
                ScreenManager.Game.Exit();*/
                return;
            }

            if (m_IsWaitingForLaunch)
            {
                if (input.IsNewButtonPress(Buttons.A, ControllingPlayer.Value) || input.IsNewKeyPress(Keys.Space, ControllingPlayer.Value))
                    m_IsWaitingForLaunch = false;
                return;
            }

            // Paddle movement
            {
                float movement = 0.0f;

                // Keyboard: full speed left/right      // TODO: improve this for poor PC players.
                if (keyboardState.IsKeyDown(Keys.Left))
                    movement--;
                if (keyboardState.IsKeyDown(Keys.Right))
                    movement++;

                // Pad: thumbstick position.
                Vector2 thumbstick = gamePadState.ThumbSticks.Left;
                movement += thumbstick.X;

                // TODO: could accumulate over time, to a maximum speed.
                movement = MathHelper.Clamp(movement, -1.0f, 1.0f);

                m_Paddle.Move(movement);
            }

            // Actions
            int playerIdx = (int)ControllingPlayer.Value;
            if (input.IsNewButtonPress(Buttons.A, ControllingPlayer.Value) || input.IsNewKeyPress(Keys.Space, ControllingPlayer.Value))
            {
                InputAJustPressed();
            }
            else
            {
                GamePadState padState = input.CurrentGamePadStates[playerIdx];
                KeyboardState keyState = input.CurrentKeyboardStates[playerIdx];
                if (padState.Buttons.A == ButtonState.Pressed || keyState.IsKeyDown(Keys.Space))
                {
                    InputAHeld();
                }
                else
                {
                    GamePadState prevPadState = input.LastGamePadStates[playerIdx];
                    KeyboardState prevKeyState = input.LastKeyboardStates[playerIdx];
                    if (prevPadState.Buttons.A == ButtonState.Pressed || prevKeyState.IsKeyDown(Keys.Space))
                        InputAJustReleased();
                }
            }

            // Vibration
            if (m_IsVibrationRequired)
            {
                m_IsVibrationRequired = false;
                input.Vibrate(ControllingPlayer.Value, 0.2f);
            }

            // Cheat codes
            #warning Cheat codes enabled!
            if (keyboardState.IsKeyDown(Keys.LeftControl) && keyboardState.IsKeyDown(Keys.W))
            {
                OnWallDestroyed();
            }
        }

        private void InputAJustPressed()
        {
            switch (m_Paddle.Type)
            {
                case Paddle.E_Types.Standard:
                    // No op.
                    break;
                case Paddle.E_Types.Laser:
                    if (m_Paddle.TryFire())
                    {
                        m_IsVibrationRequired = true;
                        m_SoundManager.Play("Laser");
                    }
                    break;
                case Paddle.E_Types.Magnet:
                    // No op.
                    break;
                case Paddle.E_Types.Glue:
                    ReleaseBalls();
                    break;
                case Paddle.E_Types.Pauser:
                    // No op.
                    break;
            }
        }

        private void InputAHeld()
        {
            switch (m_Paddle.Type)
            {
                case Paddle.E_Types.Standard:
                    // No op.
                    break;
                case Paddle.E_Types.Laser:
                    if (m_Paddle.TryFire())     // TODO: duplication
                    {
                        m_IsVibrationRequired = true;
                        m_SoundManager.Play("Laser");
                    }
                    break;
                case Paddle.E_Types.Magnet:
                    AttractBalls();
                    break;
                case Paddle.E_Types.Glue:
                    ReleaseBalls();         // TODO: [tuning] might want to enable this or not.
                    break;
                case Paddle.E_Types.Pauser:
                    FreezeBalls();
                    break;
            }
        }

        private void InputAJustReleased()
        {
            switch (m_Paddle.Type)
            {
                case Paddle.E_Types.Standard:
                    // No op.
                    break;
                case Paddle.E_Types.Laser:
                    // No op.
                    break;
                case Paddle.E_Types.Magnet:
                    // No op.
                    break;
                case Paddle.E_Types.Glue:
                    // No op.
                    break;
                case Paddle.E_Types.Pauser:
                    ReleaseBalls();
                    break;
            }
        }

        #endregion

        #region Render
        /// <summary>
        /// Draws the gameplay screen.
        /// </summary>
        public override void Draw(GameTime gameTime)
        {
            ScreenManager.GraphicsDevice.Clear(ClearOptions.Target, Color.Magenta, 0, 0);
            m_Starfield.Draw(m_StarfieldPosition);

            float timeFactor = gameTime.ElapsedGameTime.Milliseconds / 1000.0f;

            m_Paddle.Draw(timeFactor);

            foreach (Ball ball in m_Balls)
                ball.Draw(timeFactor);

            m_Wall.Draw(timeFactor);

            foreach (Drop drop in DropPool)
                drop.Draw(timeFactor);

            if (m_IsWaitingForLaunch)
            {
                DrawPopup();
            }
            else
            {
                if (m_Messages.Count > 0)
                {
                    BonusMessage message = m_Messages[0];
                    Vector2 textPos = new Vector2(1280 / 2.0f, 720 / 2.0f) - (m_GameFont.MeasureString(message.m_Message) / 2.0f);
                    Color color = ColorMath.PulseColor(message.m_Color, message.m_Timer, BonusMessage.MESSAGE_DURATION);
                    Color outline = new Color(Color.LightGray, color.A);
                    m_SpriteBatch.Begin();
                    m_SpriteBatch.DrawString(m_GameFont, message.m_Message, textPos + Vector2.One, outline);
                    m_SpriteBatch.DrawString(m_GameFont, message.m_Message, textPos - Vector2.One, outline);
                    m_SpriteBatch.DrawString(m_GameFont, message.m_Message, textPos + new Vector2(1, -1), outline);
                    m_SpriteBatch.DrawString(m_GameFont, message.m_Message, textPos + new Vector2(-1, 1), outline);
                    m_SpriteBatch.DrawString(m_GameFont, message.m_Message, textPos, color);
                    m_SpriteBatch.End();
                }
            }

            m_SpriteBatch.Begin();

            float yCurr = 82.0f;

            Vector2 levelPosition = new Vector2(m_Wall.GetBoundingBox().Right + 32.0f, yCurr);
            const string level = "Level:";
            m_SpriteBatch.DrawString(m_GameFont, level, levelPosition + Vector2.One, Color.Black);
            m_SpriteBatch.DrawString(m_GameFont, level, levelPosition, Color.White);
            yCurr += 32.0f;
            levelPosition.Y = yCurr;
            m_SpriteBatch.DrawString(m_GameFont, m_LevelName, levelPosition, Color.White);

            yCurr += 64.0f;

            Vector2 scorePosition = new Vector2(m_Wall.GetBoundingBox().Right + 32.0f, yCurr);
            const string score = "Score:";
            m_SpriteBatch.DrawString(m_GameFont, score, scorePosition + Vector2.One, Color.Black);
            m_SpriteBatch.DrawString(m_GameFont, score, scorePosition, Color.White);
            yCurr += 32.0f;
            scorePosition.Y = yCurr;
            m_SpriteBatch.DrawString(m_GameFont, m_ScoreString, scorePosition, Color.White);

            if (m_PlaySession.m_Mode != E_GameModes.Marathon && m_PlaySession.m_Mode != E_GameModes.TimeAttack)
            {
                DrawLives(ref yCurr);
            }

            if (m_PlaySession.m_SecondsRemaining > 0.0f)
            {
                DrawTimeLeft(ref yCurr);
            }
            
            m_SpriteBatch.End();

            // If the game is transitioning on or off, fade it out to black.
            if (TransitionPosition > 0)
                ScreenManager.FadeBackBufferToBlack(255 - TransitionAlpha);
        }

        #region HUD helpers methods
        private void DrawPopup()
        {
            Vector2 rectPos = new Vector2(1280 / 2.0f - 256.0f, 280.0f);
            Vector2 rectSize = new Vector2(512.0f, 200.0f);
            m_PrimitiveBatch.FillRectangle(rectPos, rectSize, new Color(0, 0, 0, 150));
            m_PrimitiveBatch.DrawRectangle(rectPos, rectSize, new Color(255, 255, 255, 150));

            m_SpriteBatch.Begin();

            float textY;

            if (m_PlaySession.m_Mode != E_GameModes.Marathon && m_PlaySession.m_Mode != E_GameModes.TimeAttack)
            {
                const string lives = "Remaining lives:";
                Vector2 livesPos = new Vector2(1280 / 2.0f, 320.0f) - (m_GameFont.MeasureString(lives) / 2.0f);
                m_SpriteBatch.DrawString(m_GameFont, lives, livesPos, Color.White);

                float deltaX = 48.0f;
                float scale = 0.4f;
                int livesCount = (m_PlaySession.m_LivesRemaining + 1);
                Vector3 position = new Vector3((1280 / 2.0f) - (((livesCount / 2.0f) - 0.5f) * deltaX), 350.0f, 0.0f);
                for (int i = 0; i != livesCount; i++)
                {
                    m_Paddle.DrawSmall(position, scale, Color.White, true);
                    position.X += deltaX;
                }

                textY = 420.0f;
            }
            else
            {
                textY = 380.0f;
            }

            const string text = "Press A to launch";
            Vector2 textPos = new Vector2(1280 / 2.0f, textY) - (m_GameFont.MeasureString(text) / 2.0f);
            m_SpriteBatch.DrawString(m_GameFont, text, textPos, Color.White);
            m_SpriteBatch.End();
        }

        private void DrawLives(ref float yCurr)
        {
            float baseX = m_Wall.GetBoundingBox().Right + 32.0f;
            yCurr += 64.0f;
            Vector2 livesPosition = new Vector2(baseX, yCurr);
            const string lives = "Lives:";
            m_SpriteBatch.DrawString(m_GameFont, lives, livesPosition + Vector2.One, Color.Black);
            m_SpriteBatch.DrawString(m_GameFont, lives, livesPosition, Color.White);

            yCurr += 32.0f;

            float maxX = 1280.0f * 0.9f;        // Extended safe frame.
            float deltaX = 16.0f;
            float deltaY = 48.0f;
            float scale = 0.4f;
            Vector3 position = new Vector3(baseX + 4.0f, yCurr + (deltaY / 2.0f), 0.0f);

            int livesCount = (m_PlaySession.m_LivesRemaining + 1);
            for (int i = 0; i != livesCount; i++)
            {
                if (position.X > maxX)
                {
                    position.X = baseX + 4.0f;
                    position.Y += deltaY;

                    yCurr += deltaY;
                }

                m_Paddle.DrawSmall(position, scale, Color.White, false);
                position.X += deltaX;
            }
            yCurr += (deltaY / 2.0f);
        }

        private void DrawTimeLeft(ref float yCurr)
        {
            yCurr += 64;
            Vector2 timePosition = new Vector2(m_Wall.GetBoundingBox().Right + 32.0f, yCurr);
            const string time = "Time left:";
            m_SpriteBatch.DrawString(m_GameFont, time, timePosition + Vector2.One, Color.Black);
            m_SpriteBatch.DrawString(m_GameFont, time, timePosition, Color.White);

            yCurr += 32;
            timePosition.Y = yCurr;
            m_SpriteBatch.DrawString(m_GameFont, m_TimeString, timePosition, Color.White);

        }
        #endregion

        #endregion

        #region Public API

        private static readonly int[] SCORES_BRICK =
        {
            100,
            500,
            1000,
            200,
        };

        private const int SCORE_DROP = 20;
        private const int SCORE_BONUS_LIFE = 1000;
        private const int SCORE_BONUS_SECOND = 50;

        public void OnBrickDestroyed(Brick brick)
        {
            Random rand = SibLib.RandomMath.Random;
            if (rand.Next(100) < m_Wall.BonusPercent)                   // TODO: is it hit based, or history based?
            {
                Drop drop = DropPool.AddNewItem();
                int type = rand.Next((int)Drop.E_DropTypes.Count_);     // TODO: list available bonuses for each level.
                drop.Type = (Drop.E_DropTypes)type;
                drop.Position = brick.Position;
                drop.Initialize();
                drop.IsActive = true;
            }

            if (brick.Type == Brick.E_Types.Nitro)
            {
                ExplodeBricks(brick, brick.Size * 2.0f);
                // TODO: special sound
            }

            ScoreIncrease(SCORES_BRICK[(int)brick.Type]);
            
            m_ScoreString.Remove(0, m_ScoreString.Length);
            StringTools.AppendIntToStringBuilder(m_ScoreString, m_PlaySession.m_Score);

            m_SoundManager.Play("MenuValid");                           // TODO: Well, sound design is limited ;-)
        }

        public void OnWallDestroyed()
        {
            // Compute bonus scores.
            ScoreIncrease(SCORE_BONUS_LIFE * m_PlaySession.m_LivesRemaining);            

            if (m_PlaySession.m_Mode == E_GameModes.TimeAttack)
            {
                ScoreIncrease((int)(SCORE_BONUS_SECOND * m_PlaySession.m_SecondsRemaining));
            }

            // TODO: transition, etc...

            m_PlaySession.m_LevelCurrentIndex++;
            if (m_PlaySession.m_LevelCurrentIndex >= m_LevelList.Count)
            {
                LoadingScreen.Load(ScreenManager, false, ControllingPlayer, new ResultScreen(m_PlaySession));
                return;
            }

            LoadLevelCurrent();
            InitializeGameplay();
        }

        public void ExplodeBricks(Brick startBrick, Vector2 range)
        {
            foreach (Brick brick in m_Wall)
            {
                if (Math.Abs(brick.Position.X - startBrick.Position.X) <= range.X &&
                    Math.Abs(brick.Position.Y - startBrick.Position.Y) <= range.Y)
                {
                    brick.OnExplosionHit();
                }
            }
        }

        public void OnDropCollected(Drop drop)
        {
            BonusMessage message = m_Messages.AddNewItem();
            message.m_Message = drop.ToString();
            message.m_Color = drop.Color;
            message.m_Timer = BonusMessage.MESSAGE_DURATION;

            ScoreIncrease(SCORE_DROP);   

            switch (drop.Type)
            {
                case Drop.E_DropTypes.Extension:
                    m_Paddle.ScaleInc(true);
                    break;
                case Drop.E_DropTypes.Reduction:
                    m_Paddle.ScaleInc(false);
                    break;
                case Drop.E_DropTypes.Standard:
                    m_Paddle.ScaleReset();
                    m_Paddle.Type = Paddle.E_Types.Standard;
                    ReleaseBalls();
                    break;
                case Drop.E_DropTypes.Laser:
                    m_Paddle.Type = Paddle.E_Types.Laser;
                    ReleaseBalls();
                    break;
                case Drop.E_DropTypes.Magnet:
                    m_Paddle.Type = Paddle.E_Types.Magnet;
                    ReleaseBalls();
                    break;
                case Drop.E_DropTypes.Glue:
                    if (m_Paddle.Type != Paddle.E_Types.Glue)
                    {
                        m_Paddle.Type = Paddle.E_Types.Glue;
                        ReleaseBalls();
                    }
                    break;
                case Drop.E_DropTypes.Pauser:
                    if (m_Paddle.Type != Paddle.E_Types.Pauser)
                    {
                        m_Paddle.Type = Paddle.E_Types.Pauser;
                        ReleaseBalls();
                    }
                    break;
                case Drop.E_DropTypes.Baseball:
                    foreach (Ball ball in m_Balls)
                        ball.Type = Ball.E_Types.Standard;      // TODO: TUNING: set a basic speed too?
                    break;
                case Drop.E_DropTypes.Superball:
                    foreach (Ball ball in m_Balls)
                        ball.Type = Ball.E_Types.Superball;
                    break;
                case Drop.E_DropTypes.Perforaball:
                    foreach (Ball ball in m_Balls)
                        ball.Type = Ball.E_Types.Perforaball;
                    break;
                case Drop.E_DropTypes.Nitroball:
                    foreach (Ball ball in m_Balls)
                        ball.Type = Ball.E_Types.Nitroball;
                    break;
                case Drop.E_DropTypes.SpeedUp:
                    foreach (Ball ball in m_Balls)
                        ball.SpeedInc(true);
                    break;
                case Drop.E_DropTypes.SpeedDown:
                    foreach (Ball ball in m_Balls)
                        ball.SpeedInc(false);
                    break;
                case Drop.E_DropTypes.BallDouble:
                    foreach (Ball ball in m_Balls)
                        SplitBall(ball, 2);
                    break;
                case Drop.E_DropTypes.BallTriple:
                    foreach (Ball ball in m_Balls)
                        SplitBall(ball, 3);
                    break;
                case Drop.E_DropTypes.BallQuintuple:
                    foreach (Ball ball in m_Balls)
                        SplitBall(ball, 5);
                    break;
                case Drop.E_DropTypes.PadShieldTime:
                    m_Paddle.ActivateShieldTime();
                    break;
                case Drop.E_DropTypes.PadShieldHit:
                    m_Paddle.ActivateShieldHit();
                    break;
                case Drop.E_DropTypes.LevelShieldTime:
                    m_Wall.ActivateShieldTime();
                    break;
                case Drop.E_DropTypes.LevelShieldHit:
                    m_Wall.ActivateShieldHit();
                    break;
                default:
                    throw new NotImplementedException("Drop type not implemented: " + drop.Type.ToString());
            }
        }

        public void RequestVibration()
        {
            m_IsVibrationRequired = true;
        }

        #endregion

        #region Internal operations
        private void OnLastBallLost()
        {
            if (m_PlaySession.m_Mode != E_GameModes.Marathon && m_PlaySession.m_Mode != E_GameModes.TimeAttack)
            {
                m_PlaySession.m_LivesRemaining--;
                if (m_PlaySession.m_LivesRemaining < 0)
                {
                    LoadingScreen.Load(ScreenManager, false, ControllingPlayer, new ResultScreen(m_PlaySession));
                    return;
                }
            }

            ResetPaddle();
            DropPool.Clear();
            m_Messages.Clear();

            Ball ball = m_Balls.AddNewItem();
            SetupBall(ball);
            ball.Spawn();

            m_IsWaitingForLaunch = true;
        }

        private void ResetPaddle()
        {
            Rectangle wallRect = m_Wall.GetBoundingBox();
            m_Paddle.Size = new Vector2(96.0f, 24.0f);
            m_Paddle.Position = new Vector2(wallRect.Center.X, wallRect.Bottom - 8.0f);
            m_Paddle.Type = Paddle.E_Types.Standard;
            m_Paddle.IsActive = true;
            m_Paddle.Wall = m_Wall;
            m_Paddle.Initialize();      // Note: this includes computing the world matrix. If previously init before, this won't recreate any graphic data.
        }

        private void SetupBall(Ball ball)
        {
            ball.Wall = m_Wall;
            ball.Paddle = m_Paddle;
            ball.Initialize();
            ball.IsActive = true;
        }

        private void SplitBall(Ball ball, int count)
        {
            float ballSpeed = ball.Velocity.Length();
            double ballAngle = Math.Atan2(-ball.Velocity.Y, ball.Velocity.X);

            int newBallCount = Math.Min(count - 1, m_Balls.Available);
            // Max ball count reached.
            if (newBallCount == 0)
                return;

            float angleIncrement = MathHelper.PiOver2 / (float)newBallCount;

            double newAngle = ballAngle + MathHelper.PiOver4;
            ball.SpeedSet(ballSpeed, newAngle);

            for (int i = 0; i != newBallCount; i++)
            {
                Ball newBall = m_Balls.AddNewItem();            // Should never be null, we checked above.

                SetupBall(newBall);               
                newBall.Position = ball.Position;
                newBall.Type = ball.Type;

                newAngle -= angleIncrement;
                float clamped = MathHelper.WrapAngle((float)newAngle);
                if (clamped < 0.1f || (clamped > MathHelper.Pi - 0.15f && clamped < MathHelper.Pi + 0.15f))
                {
                    newAngle -= angleIncrement / 3.0f;
                }
                newBall.SpeedSet(ballSpeed, newAngle);
            }
        }

        private void FreezeBalls()
        {
            foreach (Ball ball in m_Balls)
            {
                ball.IsFrozen = true;
            }
        }

        private void ReleaseBalls()
        {
            foreach (Ball ball in m_Balls)
            {
                ball.IsFrozen = false;
            }
        }

        private void AttractBalls()
        {
            Point center = m_Paddle.GetBoundingBox().Center;
            Vector2 attractPosition = new Vector2(center.X, center.Y);
            foreach (Ball ball in m_Balls)
            {
                Vector2 delta = attractPosition - ball.Position;
                delta.Normalize();
                ball.Velocity += (delta * 15.0f);           // TODO: use angle instead.
            }
        }

        private static readonly int[] BONUS_LIFE_THRESHOLD = { 10000, 20000, 50000 };
        private const string BONUS_LIFE = "Bonus life!";

        private void ScoreIncrease(int amount)
        {
            int newValue = m_PlaySession.m_Score + amount;
            int bonusLifeTreshold = BONUS_LIFE_THRESHOLD[(int)m_PlaySession.m_Difficulty];
            if (newValue / bonusLifeTreshold > m_PlaySession.m_Score / bonusLifeTreshold)
            {
                m_PlaySession.m_LivesRemaining++;

                BonusMessage message = m_Messages.AddNewItem();
                message.m_Message = BONUS_LIFE;
                message.m_Color = Color.White;
                message.m_Timer = BonusMessage.MESSAGE_DURATION * 2.0f;
            }

            m_PlaySession.m_Score = newValue;
        }

        private void UpdateTimeString()
        {
            m_TimeString.Remove(0, m_TimeString.Length);
            StringTools.TimeToStringBuilder(m_TimeString, (int)m_PlaySession.m_SecondsRemaining);
        }

        #endregion
    }
}
